/*
 *  Copyright (C) 2010 Zubanov Dmitry
 * 
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 * 
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 * 
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
/**
 *
 * @author Zubanov Dmitry aka javadimon, zubanov@gmail.com
 * Omsk, Russia, created 18.09.2010
 * (C) Copyright by Zubanov Dmitry
 */
package org.jssdb.core.net;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import org.jssdb.core.DBProperties;
import org.jssdb.core.filesystem.ModifyAction;
import org.jssdb.core.filesystem.ModifyLogger;
import org.jssdb.core.event.CustomEvent;
import org.jssdb.core.event.CustomEventListener;
import org.jssdb.core.filesystem.AddedFilesList;
import org.jssdb.core.filesystem.DeletedFilesList;
import org.jssdb.core.filesystem.FileContainer;
import org.jssdb.core.filesystem.FileObjectInfo;
import org.jssdb.core.filesystem.FilesListInfo;
import org.jssdb.core.filesystem.Transaction;
import org.jssdb.core.handler.Handler;
import org.jssdb.core.ram.RamHandler;
import org.jssdb.core.utils.FileSeparator;

public class ReplicationServer {

    private static ReplicationServer self = null;
    private int port = 7000;
    private String host = "localhost";
    private boolean isEnable = false;
    private DataChunkCollector dataChunkCollector = DataChunkCollector.getInstance();
    private static final Logger logger = Logger.getLogger(ReplicationServer.class.getName());

    private ReplicationServer() {
        dataChunkCollector.addCustomEventListener(new CustomEventListener() {

            public void customEventOccurred(CustomEvent evt) {
                if (evt.getSource() instanceof FilesListInfo) {
                    FilesListInfo filesListInfo = (FilesListInfo) evt.getSource();
                    compareTo(filesListInfo);
                }

                if (evt.getSource() instanceof DeletedFilesList) {
                    DeletedFilesList deletedFilesList = (DeletedFilesList) evt.getSource();
                    deletedFilesHandler(deletedFilesList);
                }

                if (evt.getSource() instanceof FileContainer) {
                    ((FileContainer) evt.getSource()).writeFile();
                }

                if (evt.getSource() instanceof AddedFilesList) {
                    AddedFilesList addFilesList = (AddedFilesList) evt.getSource();
                    if (addFilesList.getStatus() == AddedFilesList.REQUEST) {
                        addFilesList.setFilesObject();
                        addFilesList.setStatus(AddedFilesList.RESPONSE);
                        sender(addFilesList.getHost(), addFilesList.getPort() - 1, addFilesList);
                    }

                    if (addFilesList.getStatus() == AddedFilesList.RESPONSE) {
                        addNewObjects(addFilesList);
                    }
                }
            }
        });
    }

    public void start() {
        if (isEnable) {
            Executors.newSingleThreadExecutor().execute(reciever());
        }
    }

    private void addNewObjects(AddedFilesList addFilesList) {
        for (FileObjectInfo fileObjectInfo : addFilesList.getAddedList()) {
            try {
                long id = Long.parseLong(fileObjectInfo.getFileName());
                long lastModified = fileObjectInfo.getLastModified();
                RamHandler.getInstance().addObject(id, lastModified, fileObjectInfo.getObject());
                logger.log(Level.INFO, "id {0}, lastModified {1}, Object {2}",
                        new Object[]{id, lastModified, fileObjectInfo.getObject()});
            } catch (Exception e) {
                logger.log(Level.SEVERE, fileObjectInfo.getFileName(), e);
            }
        }
    }

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        this.port = port;
    }

    public void setEnable(boolean isEnable) {
        this.isEnable = isEnable;
    }

    public boolean isEnable() {
        return isEnable;
    }

    public static ReplicationServer getInstance() {
        if (self == null) {
            self = new ReplicationServer();
        }

        return self;
    }

    public void sendToAllModifyAction(File file, int action) {
        if (action == ModifyAction.ADD) {
            String arr[] = file.getAbsolutePath().split(FileSeparator.getInstance().separator);
            long id = Long.parseLong(arr[arr.length - 1]);
            String className = arr[arr.length - 2];
            Object obj = Handler.getInstance().get(id, className);
            Transaction transaction = new Transaction(id, file.lastModified(), Transaction.ADD, obj);
            try {
                MulticastServer.getInstance().sendRequest(transaction);
            } catch (Exception e) {
                logger.log(Level.SEVERE, null, e);
            }

        }

        if (action == ModifyAction.MODIFY) {
            String arr[] = file.getAbsolutePath().split(FileSeparator.getInstance().separator);

            long id = Long.parseLong(arr[arr.length - 1]);
            String className = arr[arr.length - 2];
            Object obj = RamHandler.getInstance().getObjectById(id, className);

            Transaction transaction = new Transaction(id, file.lastModified(),
                    Transaction.MODIFY, obj);
            try {
                MulticastServer.getInstance().sendRequest(transaction);
            } catch (Exception e) {
                logger.log(Level.SEVERE, null, e);
            }
        }

        if (action == ModifyAction.DELETE) {
            String arr[] = file.getAbsolutePath().split(FileSeparator.getInstance().separator);
            Transaction transaction = new Transaction(Long.parseLong(arr[arr.length - 1]),
                    0, Transaction.DELETE, arr[arr.length - 2]);
            try {
                MulticastServer.getInstance().sendRequest(transaction);
            } catch (Exception e) {
                logger.log(Level.SEVERE, null, e);
            }
        }
    }

    public ArrayList<ServerInfo> getServersList() {
        return MulticastServer.getInstance().getServerInfoList();
    }

    private void deletedFilesHandler(DeletedFilesList deletedFilesList) {
        for (FileObjectInfo extFileObjectInfo : deletedFilesList.getDeletetList()) {
            for (FileObjectInfo thisFileObjectInfo : FilesListInfo.getInstance().getFilesListInfo()) {
                if (extFileObjectInfo.getClassName().equals(thisFileObjectInfo.getClassName())
                        && extFileObjectInfo.getFileName().equals(thisFileObjectInfo.getFileName())) {
                    ModifyAction extModifyAction = deletedFilesList.getModifyLogger().
                            findModifyAction(thisFileObjectInfo.getClassName(),
                            thisFileObjectInfo.getFileName(), ModifyAction.DELETE);

                    if (extModifyAction == null) {
                        continue;
                    }

                    if (extModifyAction.getTime() > thisFileObjectInfo.getLastModified()) {
                        logger.log(Level.INFO, "DELETE FILE {0}", thisFileObjectInfo);
                        RamHandler.getInstance().deleteObject(Long.parseLong(thisFileObjectInfo.getFileName()),
                                thisFileObjectInfo.getClassName());
                    }
                }
            }
        }
    }

    private void compareTo(FilesListInfo filesListInfo) {
        logger.log(Level.INFO, "{0}", filesListInfo);

        ArrayList<FileObjectInfo> notEqList = new ArrayList();
        ArrayList<FileObjectInfo> deleteList = new ArrayList();
        FilesListInfo thisFilesListInfo = FilesListInfo.getInstance();

        for (FileObjectInfo outFileInfo : filesListInfo.getFilesListInfo()) {
            boolean isFind = false;
            for (FileObjectInfo thisFileObjectInfo : thisFilesListInfo.getFilesListInfo()) {
                if (thisFileObjectInfo.equals(outFileInfo)) {
                    isFind = true;
                    break;
                }
            }

            if (!isFind) {
                String strFile = DBProperties.getInstance().getPathToDB()
                        + outFileInfo.getClassName()
                        + File.separator + outFileInfo.getFileName();
                File f = new File(strFile);
                if (f.isFile()) {
                    notEqList.add(outFileInfo);
                } else {
                    deleteList.add(outFileInfo);
                }
            }
        }

        logger.log(Level.INFO, "Modifyed files: {0}", notEqList);
        if (!notEqList.isEmpty()) {
            for (FileObjectInfo fileObjectInfo : notEqList) {
                for (FileObjectInfo thisFileObjectInfo : FilesListInfo.getInstance().getFilesListInfo()) {
                    if (thisFileObjectInfo.getLastModified() > fileObjectInfo.getLastModified()) {
                        long id = Long.parseLong(thisFileObjectInfo.getFileName());
                        Object obj = RamHandler.getInstance().getObjectById(id, thisFileObjectInfo.getClassName());
                        FileContainer fileContainer = new FileContainer(id,
                                thisFileObjectInfo.getLastModified(),
                                thisFileObjectInfo.getClassName(),
                                thisFileObjectInfo.getFileName(), obj);
                        sender(filesListInfo.getHost(), filesListInfo.getPort() - 1, fileContainer);

                    } else {
                        long id = Long.parseLong(fileObjectInfo.getFileName());
                        RamHandler.getInstance().updateObject(id, fileObjectInfo.getObject());
                        String filePath = DBProperties.getInstance().getPathToDB()
                                + fileObjectInfo.getClassName() + File.separator
                                + fileObjectInfo.getFileName();
                        File f = new File(filePath);
                        if (f.isFile()) {
                            f.setLastModified(fileObjectInfo.getLastModified());
                        }
                    }
                }
            }
        }

        logger.log(Level.INFO, "Deleted | Added files: {0}", deleteList);
        if (!deleteList.isEmpty()) {
            ArrayList<FileObjectInfo> addedFileList = new ArrayList();
            for (FileObjectInfo fileObjectInfo : deleteList) {
                ModifyAction ma = ModifyLogger.getInstance().findModifyAction(fileObjectInfo.getClassName(),
                        fileObjectInfo.getFileName(), ModifyAction.DELETE);
                if (ma == null) {
                    logger.log(Level.INFO, "FileObjectInfo for add {0}", fileObjectInfo);
                    addedFileList.add(fileObjectInfo);

                } else {
                    DeletedFilesList dfl = new DeletedFilesList(deleteList);
                    dfl.attachModifyLogger(ModifyLogger.getInstance());
                    sender(filesListInfo.getHost(), filesListInfo.getPort() - 1, dfl);
                }
            }

            if (!addedFileList.isEmpty()) {
                sender(filesListInfo.getHost(), filesListInfo.getPort() - 1,
                        new AddedFilesList(this.host, this.port, addedFileList));
            }
        }
    }

    private Runnable reciever() {
        Runnable r = new Runnable() {

            public void run() {
                try {
                    DatagramChannel channel = DatagramChannel.open();
                    DatagramSocket socket = channel.socket();
                    InetAddress addr = InetAddress.getByName(host);
                    SocketAddress address = new InetSocketAddress(addr, port - 1);
                    socket.bind(address);

                    ByteBuffer buffer = ByteBuffer.allocateDirect(65536);
                    while (true) {
                        channel.receive(buffer);
                        buffer.flip();

                        byte[] bytes = new byte[buffer.remaining()];
                        buffer.get(bytes, 0, bytes.length);
                        ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
                        GZIPInputStream gs = new GZIPInputStream(bais);
                        ObjectInputStream in = new ObjectInputStream(gs);
                        Object object = in.readObject();

                        if (object instanceof DataChunk) {
                            dataChunkCollector.addChunk((DataChunk) object);
                        }

                        buffer.clear();
                    }
                } catch (Exception e) {
                    logger.log(Level.SEVERE, host, e);
                }
            }
        };

        return r;
    }

    public void sender(String host, int port, Object obj) {

        try {
            DatagramChannel channel = DatagramChannel.open();
            channel.configureBlocking(false);
            channel.connect(new InetSocketAddress(host, port));
            Selector selector = Selector.open();
            //channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
            channel.register(selector, SelectionKey.OP_WRITE);

            while (true) {
                selector.select(10000);
                Set readyKeys = selector.selectedKeys();
                if (readyKeys.isEmpty()) {
                    break;

                } else {
                    Iterator iterator = readyKeys.iterator();
                    while (iterator.hasNext()) {
                        SelectionKey key = (SelectionKey) iterator.next();
                        iterator.remove();

                        if (key.isWritable()) {
                            ArrayList<DataChunk> dataChunkList = new DataChunkList().getDataChunkList(obj);
                            for (DataChunk dataChunk : dataChunkList) {
                                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                                GZIPOutputStream gz = new GZIPOutputStream(baos);
                                ObjectOutputStream oout = new ObjectOutputStream(gz);
                                oout.writeObject(dataChunk);
                                oout.flush();
                                oout.close();
                                gz.flush();
                                gz.close();

                                channel.write(ByteBuffer.wrap(baos.toByteArray()));
                                key.interestOps(SelectionKey.OP_READ);
                            }
                        }
                    }
                }
            }
        } catch (Exception e) {
            logger.log(Level.SEVERE, null, e);
        }
    }
}
